import numpy as np                                      #Numpy - библиотека для работы с числовыми массивами, векторами и матрицами на Python
import matplotlib.pyplot as plt                         #Matplotlib.pyplot - библиотека построения графиков для создания графиков и диаграмм
from tensorflow.keras.datasets import mnist             #Mnist - база данных рукописных цифр, которая содержит 60 000 изображений в обучающей выборке и 10 000 изображений в тестовой выборке
from tensorflow import keras                            #Импорт библиотеки Keras
from tensorflow.keras.layers import Dense, Flatten      #Dense - это класс односвязного/линейного слоя, все нейроны связаны друг с другом.


#Подрузка датасета сразу на обучение и на тест
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# x_train - изображения цифр в обучающей выборке
# y_train - вектор соответсвующих значений цифр(если на i-м элементе изображена 2-ка, то y_train[i] = 2)
# x_test - изображения цифр в тестовой выборке
# y_test - вектор соответсвующих значений цифр для тестовой выборки

#Изображения представлены в градациях серого, где 0 - чёрный цвет, а 255 - белый цвет
x_train.shape, x_test.shape
#Данные на выходе:
#((60000, 28, 28), (10000, 28, 28))
#60000 картинок размером 28 на 28 идёт на обучение
#На тест идёт 10000 картинок такого же размера


#Проверка размера картинок. Картинка должна быть размером 28 на 28 пикселей
x_train[0].shape
#Данные на выходе (28,28)




# стандартизация входных данных
# Пиксели задаются двумя способами: числами от 0 до 255 и числами от 0 до 1
# Нормируем данные, сейчас обойдемся без MinMaxScaler из sklearn, а воспользуемся делением на 255,
# т.к. сейчас изображения представлены пикселями в диапазоне от 0 до 255, 
# а для нейросети комфортней обучаться на диапазоне от 0 до 1.
x_train = x_train / 255
x_test = x_test / 255

y_train_cat = keras.utils.to_categorical(y_train, 10)
y_test_cat = keras.utils.to_categorical(y_test, 10)

# Следующий код выводит на экран 5 изображений из обучающего набора с использованием библиотеки matplotlib. 
# Это создает фигуру с 1 строкой и 5 столбцами размером 15x10.
# В цикле for каждое изображение воспроизводится с помощью imshow с серой цветовой картой, а ось отключается с помощью axis('of'). 
# Это создает визуальное представление изображений, позволяя нам убедиться, что они были загружены правильно.
# стандартизация входных данных
plt.figure(figsize=(10,5))
for i in range(25):
    plt.subplot(5,5,i+1)
    plt.xticks([])
    plt.yticks([])
    plt.imshow(x_train[i], cmap=plt.cm.binary)

# Проверка разметки
y_train[:5]
# Данные на выходе 
# array([5, 0, 4, 1, 9], dtype=uint8

model = keras.Sequential([
    #Слой Flatten преобразует матрицу размером 28 на 28 пикселей в последовательность чисел, состоящщей в данном случае из 784 значений 
    #Другими словами, слой Flatten каждый из 28 слоёв, состоящий из 28 пикселей, преобразует в одну сплошную линию, состоящую из 794 пикселей
    Flatten(input_shape=(28, 28, 1)),                   #На первом слое будет 28x28 = 784 входа, на каждый пиксель по входу + 1 bias. 
   
    #В этом слое содержится 100 нейронов. Слой Dense автоматически организует связи между 100 нейронами и 784 входными значениями
    Dense(100, activation='relu'), 
    #В этом слое содержится 10 нейронов, по одному нейрону на каждую цифру. Функия активации softmax, так как мы хотим интерпритировать выходящие значения как вероятность принадлежности к одному или другому классу цифр
    Dense(10, activation='softmax')                     
])

print(model.summary())      # вывод структуры нейронной сети в консоль



# Данный код относится к компиляции нейронной сети в Keras с помощью метода compile().

# optimizer='adam' задает метод оптимизации, используемый в процессе обучения. В данном случае используется алгоритм оптимизации Adam.

# loss='categorical_crossentropy' указывает функцию потерь, которую будет оптимизировать сеть. Здесь используется перекрестная энтропия между предсказанными и истинными метками классов, так как решается задача многоклассовой классификации.

# metrics=['accuracy'] указывает метрику, которую следует использовать для оценки производительности модели. Здесь используется точность (accuracy), которая показывает, какая доля предсказанных меток классов соответствует истинным меткам.
# Метод оптимизации Adam (Adaptive Moment Estimation) - это алгоритм стохастической оптимизации, который используется для обучения нейронных сетей и других моделей машинного обучения. Он был предложен Diederik P. Kingma и Jimmy Ba в 2015 году.

# Алгоритм Adam сочетает в себе идеи двух других методов оптимизации - алгоритма градиентного спуска с моментом (SGD with momentum) и алгоритма RMSProp (Root Mean Square Propagation).

# Основная идея метода Adam заключается в том, что он вычисляет два момента для каждого параметра модели - первый момент (усредненный градиент) и второй момент (усредненный квадрат градиента). Затем эти моменты используются для обновления весов модели.

# Конкретно, для каждого параметра w метод Adam вычисляет:

# Градиент функции потерь по параметру: g.

# Экспоненциально сглаженные оценки первого (m) и второго (v) моментов градиента:

# m = beta1 * m + (1 - beta1) * g
# v = beta2 * v + (1 - beta2) * g^2

# где beta1 и beta2 - параметры, определяющие скорость забывания для первого и второго моментов соответственно. Обычно они берутся равными 0.9 и 0.999.

# Использует эти оценки для обновления весов:

# w = w - alpha * m / (sqrt(v) + epsilon)

# где alpha - скорость обучения (learning rate), а epsilon - маленькое число для избежания деления на ноль.

# Одна из особенностей метода Adam - адаптивность скорости обучения. Это означает, что он может автоматически изменять скорость обучения для каждого параметра в зависимости от значений первого и второго моментов градиента. Это может быть полезно в случаях, когда некоторые параметры обновляются сильно, а другие - слабо.
# После выполнения данной команды, модель будет готова к обучению на тренировочном наборе данных с помощью метода fit().
#Оптимизация по adam, отпимизация функции потери loss идёт по кросс-энтропии. Кросс-энтропия лучше всего подходит в задачах классификации
#метрика идёт по уменьшению процента, чтобы в процессе работы нейросети можно было отследить процент увеличения точности работы нейронной сети
model.compile(optimizer='adam',
             loss='categorical_crossentropy',
             metrics=['accuracy'])

#Запуск процесса обучения нейросети путём передачи входных данных, выходный данных, а так же говорим, что данные надо обрабатывать группой размером по 32 единицы. Такую операцию надо произвести 5 раз
model.fit(x_train, y_train_cat, batch_size=32, epochs=5, validation_split=0.2)
model.evaluate(x_test, y_test_cat)



#Проверка работоспособности нейросети
n = 1
x = np.expand_dims(x_test[n], axis=0)
res = model.predict(x)
print( res )
print( np.argmax(res) )

plt.imshow(x_test[n], cmap=plt.cm.binary)
plt.show()